package com.soccerlover9527.jadis.datasturct.impl;

import com.soccerlover9527.jadis.datasturct.AbstractJadisObject;
import com.soccerlover9527.jadis.datasturct.JString;
import com.soccerlover9527.jadis.enums.DataType;
import com.soccerlover9527.jadis.exception.IncrDecrOverflowException;
import com.soccerlover9527.jadis.exception.JadisException;
import com.soccerlover9527.jadis.exception.OperationNotSupportException;
import com.soccerlover9527.jadis.exception.StringIncrDecrException;
import com.soccerlover9527.jadis.util.BitUtil;
import com.soccerlover9527.jadis.util.StringUtil;

import java.util.Objects;

/**
 * <p></p>
 *
 * @author soccerlover9527@gmail.com
 * @since 2023-08-08
 */
public class JadisString extends AbstractJadisObject implements JString {
    /**
     * max long has 20 characters !!! {@link Long#MIN_VALUE}
     */
    private static final int MAX_LONG_BYTE_LENGTH = 20;
    private JString delegate;

    public static JadisString of(String str) {
        return new JadisString(str);
    }

    public JadisString(String str) {
        super(DataType.STRING);
        if (str.length() <= MAX_LONG_BYTE_LENGTH) {
            //  try to compress to long
            try {
                //  "0123" use string instead of digital
                //  "0" use digital string
                if (str.charAt(0) != '0' && str.length() > 1) {
                    var num = StringUtil.stringToLong(str);
                    delegate = new DigitalString(num);
                    return;
                }
            } catch (JadisException e) {
                //  ignore
            }
        }
        delegate = new JDKString(str);
    }

    private static class JDKString implements JString {
        String data;

        private JDKString(String str) {
            this.data = str;
        }

        private void checkOffset(int offset) {
            if (offset < 0 || offset >= data.length()) {
                throw new IndexOutOfBoundsException("offset out of range");
            }
        }

        @Override
        public String getRange(int begin, int end) {
            return data.substring(begin, end);
        }

        @Override
        public String getSet(String value) {
            var temp = data;
            this.data = value;
            return temp;
        }

        @Override
        public byte setBit(byte bitValue, int offset) {
            BitUtil.checkBitValue(bitValue);
            var bytes = data.getBytes();
            if (offset < 0 || offset > bytes.length * 8) {
                throw new IndexOutOfBoundsException("");
            }
            //  figure out the bit position in byte array
            var byteOffset = offset / 8;
            var bitOffset = offset % 8;

            //  get origin bit value
            var origin = BitUtil.getBit(bytes[bitOffset], bitOffset);
            //  set a bit into origin byte
            bytes[byteOffset] = BitUtil.setBit(origin, bitValue, bitOffset);
            //  generate new data
            data = new String(bytes);
            return origin;
        }

        @Override
        public byte getBit(int offset) {
            byte[] bytes = data.getBytes();
            //  figure out the bit position in byte array
            var byteOffset = offset / 8;
            var bitOffset = offset % 8;
            return BitUtil.getBit(bytes[byteOffset], bitOffset);
        }

        @Override
        public String setRange(int offset, String value) {
            checkOffset(offset);
            if (offset == data.length() - 1) {
                data = data.concat(value);
            }
            //  new value larger than old
            if (value.length() >= data.length()) {
                if (offset == 0) {
                    //  cover all when offset is 0
                    data = value;
                } else {
                    //  concat old value sub string with new value
                    data = data.substring(0, offset) + value;
                }
            } else {
                char[] charArray = data.toCharArray();
                if (value.length() + offset > charArray.length) {
                    data = data.substring(0, offset) + value;
                } else {
                    //  i to old array index.
                    //  j to new array index
                    for (int i = offset, j = 0; j < value.length(); i++, j++) {
                        charArray[i] = value.charAt(j);
                    }
                    data = new String(charArray);
                }
            }
            return data;
        }

        @Override
        public int append(String value) {
            data = data + value;
            return data.length();
        }

        @Override
        public long increment() {
            throw new StringIncrDecrException();
        }

        @Override
        public long incrementBy(long value) {
            throw new StringIncrDecrException();
        }

        @Override
        public long decrement() {
            throw new StringIncrDecrException();
        }

        @Override
        public long decrementBy(long value) {
            throw new StringIncrDecrException();
        }

        @Override
        public int length() {
            return data.length();
        }

        @Override
        public String toString() {
            return data;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            JDKString jdkString = (JDKString) o;

            return Objects.equals(data, jdkString.data);
        }

        @Override
        public int hashCode() {
            return data != null ? data.hashCode() : 0;
        }
    }

    private static class DigitalString implements JString {
        long data;

        private DigitalString(long data) {
            this.data = data;
        }

        @Override
        public String getRange(int begin, int end) {
            return String.valueOf(data).substring(begin, end);
        }

        @Override
        public String getSet(String value) {
            var temp = data;
            data = StringUtil.stringToLong(value);
            return String.valueOf(temp);
        }

        @Override
        public byte setBit(byte bitValue, int offset) {
            throw new OperationNotSupportException("digital string not support bit operation");
        }

        @Override
        public byte getBit(int offset) {
            throw new OperationNotSupportException("digital string not support bit operation");
        }

        @Override
        public String setRange(int offset, String value) {
            throw new OperationNotSupportException("digital string not support range operation");
        }

        @Override
        public int append(String value) {
            throw new OperationNotSupportException("digital string not support append operation");
        }

        @Override
        public long increment() {
            if (data == Long.MAX_VALUE) {
                throw new IncrDecrOverflowException();
            }
            return ++data;
        }

        @Override
        public long incrementBy(long value) {
            if (Long.MAX_VALUE - data < value) {
                throw new IncrDecrOverflowException();
            }
            return data += value;
        }

        @Override
        public long decrement() {
            if (Long.MIN_VALUE == data) {
                throw new IncrDecrOverflowException();
            }
            return --data;
        }

        @Override
        public long decrementBy(long value) {
            if (data < 0 && (data - Long.MIN_VALUE < value)) {
                throw new IncrDecrOverflowException();
            }
            return data -= value;
        }

        @Override
        public int length() {
            if (data == 0) {
                return 1;
            }
            var temp = data;
            var count = 0;
            if (temp < 0) {
                count++;
            }
            while (temp != 0) {
                temp /= 10;
                count++;
            }
            return count;
        }

        @Override
        public String toString() {
            return String.valueOf(data);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            DigitalString that = (DigitalString) o;

            return data == that.data;
        }

        @Override
        public int hashCode() {
            return Long.hashCode(data);
        }
    }

    @Override
    public String getRange(int begin, int end) {
        return delegate.getRange(begin, end);
    }

    @Override
    public String getSet(String value) {
        return delegate.getSet(value);
    }

    @Override
    public byte setBit(byte bitValue, int offset) {
        try {
            return delegate.setBit(bitValue, offset);
        } catch (OperationNotSupportException e) {
            delegate = new JDKString(delegate.toString());
        }
        return delegate.setBit(bitValue, offset);
    }

    @Override
    public byte getBit(int offset) {
        try {
            return delegate.getBit(offset);
        } catch (OperationNotSupportException e) {
            delegate = new JDKString(delegate.toString());
        }
        return delegate.getBit(offset);
    }

    @Override
    public String setRange(int offset, String value) {
        try {
            return delegate.setRange(offset, value);
        } catch (OperationNotSupportException e) {
            delegate = new JDKString(delegate.toString());
        }
        return delegate.setRange(offset, value);
    }

    @Override
    public int append(String value) {
        try {
            return delegate.append(value);
        } catch (OperationNotSupportException e) {
            delegate = new JDKString(delegate.toString());
        }
        return delegate.append(value);
    }

    @Override
    public long increment() {
        return delegate.increment();
    }

    @Override
    public long incrementBy(long value) {
        return delegate.incrementBy(value);
    }

    @Override
    public long decrement() {
        return delegate.decrement();
    }

    @Override
    public long decrementBy(long value) {
        return delegate.decrementBy(value);
    }

    @Override
    public int length() {
        return delegate.length();
    }

    @Override
    public String toString() {
        return delegate.toString();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        JadisString that = (JadisString) o;

        return Objects.equals(delegate, that.delegate);
    }

    @Override
    public int hashCode() {
        return delegate != null ? delegate.hashCode() : 0;
    }
}
